﻿using System;
using System.Collections.Generic;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Diagnostics;
using System.Linq;
//using Microsoft.Data.Entity.Design.DatabaseGeneration;
using System.Data.EntityClient;

namespace EntityGraph.SQL
{
    internal partial class TypeTree : IEnumerable<TypeTree>
    {
        private ObjectContext ObjectContext { get; set; }
        private static int typeId = 0;
        public TypeTree ParentType { get; private set; }
        public EntityType EntityType { get; private set; }
        public string EntitySetName { get; private set; }
        public Type Type { get; private set; }
        public bool IsBaseType { get; internal set; }
        public int TypeId { get; private set; }
        public string TableName { get; private set; }
        public TypeTree this[int typeId]
        {
            get
            {
                return this.Single(n => n.TypeId == (int)typeId);
            }
        }
        public TypeTree this[Type type]
        {
            get
            {
                return this.SingleOrDefault(node => node.EntityType.Name == type.Name);
            }
        }
        public TypeTree this[EntityType type]
        {
            get
            {
                return this.SingleOrDefault(node => node.EntityType == type);
            }
        }
        public IEnumerable<EdmProperty> Properties
        {
            get
            {
                return EntityType.Properties.Where(prop => prop.DeclaringType == EntityType);
            }
        }
        public static TypeTree Create(ObjectContext ctx, Type type)
        {
            return Create(ctx, type, ctx.MetadataWorkspace.GetItems<EntityType>(DataSpace.CSpace).Single(et => et.Name == type.Name)); 
        }
        private static TypeTree Create(ObjectContext ctx, Type type, EntityType entityType)
        {
            TypeTree tree = new TypeTree(ctx, type, entityType, null);
            return tree;
        }
        private TypeTree(ObjectContext ctx, Type type, EntityType entityType, TypeTree parent)
        {
            this.ObjectContext = ctx;
            this.Type = type;
            this.EntityType = entityType;
            Debug.Assert(EntityType != null);
            this.ParentType = parent;
            var rootType = entityType.GetRootOrSelf();
            if(ParentType == null)
            {
                if(rootType != entityType)
                {
                    ParentType = BuildTypePathToRootType(type, entityType);
                }
            }
            if(rootType == entityType)
            {
                TableName = RootTableName;
                EntitySetName = GetEntitySetName(EntityType);
            }
            else
            {
                TableName = RootTableName + "_" + EntityType.Name;
            }
            this.TypeId = typeId++;

            var entityTypesContainer = ctx.MetadataWorkspace.GetItems<EntityType>(DataSpace.CSpace);
            foreach(var subType in entityTypesContainer.Where(e => e.BaseType == this.EntityType).OfType<EntityType>())
            {
                if(DirectSubTypes == null)
                {
                    DirectSubTypes = new List<TypeTree>();
                    IsBaseType = true;
                }
                var subTypeName = type.Namespace + "." + subType.Name;
                DirectSubTypes.Add(new TypeTree(ctx, type.Assembly.GetType(subTypeName), subType, this));
            }
        }
        private string GetEntitySetName(EntityType entityType)
        {
            Debug.Assert(EntityType != null);
            string containerName = ObjectContext.DefaultContainerName;
            var container = ObjectContext.MetadataWorkspace.GetEntityContainer(containerName, DataSpace.CSpace);
            var entitySet = container.BaseEntitySets.Single(m => m.ElementType == entityType);
            return entitySet.EntityContainer.Name + "." + entitySet.Name;
        }
        private TypeTree BuildTypePathToRootType(Type type, EntityType entityType)
        {
            var rootType = entityType.GetRootOrSelf();
            Debug.Assert(rootType != entityType);
            EntityType baseType = (EntityType)entityType.BaseType;
            TypeTree baseTree = new TypeTree
            {
                ObjectContext = this.ObjectContext,
                IsBaseType = true,
                Type = type,
                EntityType = baseType,
                TypeId = typeId++,
                DirectSubTypes = new List<TypeTree> { this }
            };
            if(baseType == rootType)
            {
                baseTree.TableName = RootTableName;
                baseTree.EntitySetName = GetEntitySetName(baseTree.EntityType);
            }
            else
            {
                baseTree.ParentType = baseTree.BuildTypePathToRootType(type.BaseType, baseType);
                baseTree.TableName = RootTableName + "_" + baseType.Name;
            }
            return baseTree;
        }
        public static TypeTree Unify(TypeTree tree1, TypeTree tree2)
        {
            // Is tree2 a subtree of tree1?
            if(tree1.Any(t => t.EntityType == tree2.EntityType))
            {
                return tree1;
            }
            // Is tree1 a subtree of tree2?
            if(tree2.Any(t => t.EntityType == tree1.EntityType))
            {
                return tree2;
            }
            // Have both trees a common base type?
            var base1 = tree1.Root;
            var base2 = tree2.Root;
            if(base1.EntityType == base2.EntityType)
            {
                while(
                    base1.EntityType == base2.EntityType &&
                    base1.IsBaseType &&
                    base2.IsBaseType &&
                    base1.DirectSubTypes.Count == 1 &&
                    base2.DirectSubTypes.Count == 1)
                {
                    base1 = base1.DirectSubTypes.Single();
                    base2 = base2.DirectSubTypes.Single();
                }

                var subTypeName = base2.ParentType.Type.Namespace + "." + base2.ParentType.EntityType.Name;
                var subTypeName1 = base2.ParentType.EntityType.NamespaceName+ "." + base2.ParentType.EntityType.Name;
                // just to test if we can replace code for subtypename by 
                // that for subtypename2
                Debug.Assert(subTypeName == subTypeName1); 

                return TypeTree.Create(base2.ObjectContext, base2.Type.Assembly.GetType(subTypeName), base2.ParentType.EntityType);
            }
            throw new Exception("Cannot unify type trees.");            
        }
        private TypeTree() { }
        private string RootTableName
        {
            get
            {
                var rootType = EntityType.GetRootOrSelf();
                var itemCollection = ObjectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSpace);
                var entitySet = itemCollection.GetAllEntitySets().Single(set => set.ElementType == rootType);
                return entitySet.GetTableName();
            }
        }
        public TypeTree Root
        {
            get
            {
                TypeTree tree = this;
                while(tree.ParentType != null)
                {
                    tree = tree.ParentType;
                }
                return tree;
            }
        }
        public List<TypeTree> DirectSubTypes { get; private set; }
        public IEnumerator<TypeTree> GetEnumerator()
        {
            yield return this;
            if(IsBaseType)
            {
                foreach(var childType in DirectSubTypes)
                {
                    if(childType.IsBaseType)
                    {
                        foreach(var t in childType as TypeTree)
                        {
                            yield return t;
                        }
                    }
                    else
                    {
                        yield return childType;
                    }
                }
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public override string ToString()
        {
            return this.EntityType.Name;
        }
    }
}
